List Table -näkymien rakentaminen

List Table on WordPressin hallintapaneelissa oleva artikkelinäkymä, se perusnäkymä, jota voi katsella esimerkiksi Artikkelit-otsikon alla. Mitäpä jos tällaisen haluaisi rakennella itse johonkin omaan käyttötarkoitukseen?

Minulle sattui tällainen tarkoitus Lautapelioppaassa, jossa olen parannellut vanhoja lautapeliarvosteluja lisäämällä niihin pelimekaniikka-avainsanoja. Olisi vallan käytännöllistä saada hallintapaneeliin näppärä listaus, jossa olisi ne peliarvostelut, joilta pelimekaniikat puuttuvat. Tällaista listausta ei muuten saa WordPressin hallintapaneelista mistään.

Rakennellaan siis!

Aloitetaan määrittelemällä valikkosivu, johon listaus tulee. Se tehdään add_menu_page()-funktiolla:

add_action(
	'admin_menu',
	function() {
		add_menu_page(
			'Puuttuvat pelimekaniikat',       // Sivun otsikko
			'Puuttuvat pelimekaniikat',       // Valikossa näkyvä otsikko
			'manage_options',                 // Vaaditut oikeudet (admin)
			'puuttuvat_pelimekaniikat_lista', // Menun slug
			'puuttuvat_pelimekaniikat_lista', // Funktion nimi
			'dashicons-testimonial'           // Ikoni
		);
	}
);

Lisäksi tarvitaan valikkosivun toteuttava funktio:

/**
 * Tekee puuttuvien pelimekaniikkojen sivun.
 */
function puuttuvat_pelimekaniikat_lista() {
	require 'class-pelimekaniikat-list-table.php';
	$lista = new Pelimekaniikat_List_Table();
	$lista->prepare_items();
	?>
	<div class="wrap">
	<h2>Puuttuvat pelimekaniikat</h2>
	<form id="pelimekaniikkalista" method="post">
		<input type="hidden" name='page' value="<?php echo $_REQUEST['page']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped,WordPress.Security.NonceVerification ?>" />
	<?php $lista->display(); ?>
	</form>
	</div>
	<?php
}

Tässäkään ei vielä varsinaisesti päästä homman pihviin. Se pihvi on class-pelimekaniikat-list-table.php-tiedostossa määriteltävä Pelimekaniikat_List_Table-luokka, joka tässä instantioidaan, valmistellaan (prepare_items()) ja sitten näytetään display()). Lisäksi tässä kääräistään <form>-elementti taulukon ympärille ja annetaan sille page-parametri, jotta homma pelaa kuten pitää.

Sitten se pihvi, joka on WP_List_Table-tyyppinen olio. Huomaa, että dokumentaatiossa sanotaan näin:

Note: This class’s access is marked as private. That means it is not intended for use by plugin and theme developers as it is subject to change without warning in any future WordPress release. If you would still like to make use of the class, you should make a copy to use and distribute with your own project, or else use it at your own risk.

Varoituksista ei tässä tarvitse piitata. Käytännössä luokka on ollut pitkään stabiili. Jos tällä tekisi jotain pluginia tai muuta jaeltavaa, niin toki kannattaa ottaa oma kopio luokasta ja käyttää sitä, mutta tällaisessa omassa yksityisessä käytössä tuo toimii ihan hyvin noinkin.

<?php
/**
 * Pelimekaniikat List Table
 *
 * @package puuttuvat_pelimekaniikat
 */

if ( ! class_exists( 'WP_List_Table' ) ) {
	require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
}

/**
 * Listaa arvostelut, joilta puuttuu pelimekaniikat.
 */
class Pelimekaniikat_List_Table extends WP_List_Table {
	/**
	 * Rakentajafunktio.
	 */
	public function __construct() {
		global $status, $page;
		parent::__construct(
			array(
				'singular' => 'arvostelu',
				'plural'   => 'arvostelut',
				'ajax'     => false,
			)
		);
	}

	/**
	 * Tulostaa peliarvostelusarakkeen sisällön.
	 *
	 * @param WP_Post $item Tulostettava artikkeli.
	 */
	public function column_peliarvostelu( $item ) {
		$actions = array(
			'edit' => '<a href="' . get_edit_post_link( $item->ID ) . '">Muokkaa</a>',
		);
		return sprintf(
			'<strong><a href="%1$s">%2$s</a></strong> %3$s',
			get_permalink( $item->ID ),
			$item->post_title,
			$this->row_actions( $actions ),
		);
	}

	/**
	 * Palauttaa sarakkeet.
	 *
	 * @return array Sisäinen nimi => näkyvä nimi.
	 */
	public function get_columns() {
		$columns = array(
			'peliarvostelu' => 'Peliarvostelu',
		);

		return $columns;
	}

	/**
	 * Hakee artikkelit esitettäväksi listassa.
	 */
	public function prepare_items() {
		if ( isset( $_REQUEST['_wpnonce'] ) ) {
			wp_verify_nonce( $_REQUEST['_wpnonce'], 'bulk-' . $this->_args['plural'] );
		}
		$tax_query = array(
			'relation' => 'AND',
			array(
				'taxonomy' => 'pelimekaniikat',
				'operator' => 'NOT EXISTS',
			),
			array(
				'taxonomy' => 'category',
				'field'    => 'slug',
				'terms'    => array( 'peliarvostelut' ),
			),
		);

		$all_posts = get_posts(
			array(
				'post_type'      => 'post',
				'post_status'    => 'publish',
				'tax_query'      => $tax_query,
				'posts_per_page' => -1,
				'fields'         => 'ids',
			),
		);

		$total_items = count( $all_posts );

		$paged    = isset( $_REQUEST['paged'] ) ? max( 0, intval( $_REQUEST['paged'] ) ) : 0;
		$per_page = 25;

		$this->items = get_posts(
			array(
				'post_type'      => 'post',
				'post_status'    => 'publish',
				'tax_query'      => $tax_query,
				'posts_per_page' => $per_page,
				'paged'          => $paged,
				'orderby'        => 'meta_value_num',
				'meta_key'       => 'ranking_pisteet',
				'order'          => 'DESC',
			)
		);

		$columns = $this->get_columns();
		$hidden  = array();

		$this->_column_headers = array( $columns, $hidden, array() );

		$this->set_pagination_args(
			array(
				'total_items' => $total_items,
				'per_page'    => $per_page,
				'total_pages' => ceil( $total_items / $per_page ),
			)
		);
	}
}

Olennaista on siis prepare_items()-funktio, jossa haetaan ensin kaikki halutut artikkelit, jotta voidaan laskea sivujen kokonaismäärä ja sitten nykyisen sivun (selviää parametristä $_REQUEST['paged']) artikkelit (jotka tässä järjestetään ranking_pisteet-metakentän mukaan). get_columns() palauttaa listan näytettävistä sarakkeista ja näiden sarakkeiden nimien perusteella kutsutaan vastaavia column_-funktioita tulostamaan itse sarakkeiden sisältö. Koska mitään suurempia toimintoja ei tehdä, tämä kaikki on kovin suoraviivaista.

Listan tulostavaa display_items()-funktiota ei tarvitse kirjoittaa itse, se tulee WP_List_Table-luokasta perintönä.

Tältä se sitten näyttää:

Puuttuvat pelimekaniikat -näkymä

Tämä on hyvin pelkistetty esimerkki, koska mukana ei ole lainkaan bulkkitoimintoja, joita voisi tehdä kaikkiin listan artikkeleihin kerralla. Se on yksi tällaisten listanäkymien vahvuuksista, joten otetaan vielä toinen esimerkki Kirjavinkit.fi:n puolelta. Tässä on mukana bulk actions -ominaisuuksiakin:

<?php
/**
 * Alkukieli List Table
 *
 * @package kirjavinkit-ominaisuudet
 */

if ( ! class_exists( 'WP_List_Table' ) ) {
	require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
}

/**
 * Listaa kirjat, joilta puuttuu alkukieli.
 */
class Alkukieli_List_Table extends WP_List_Table {
	/**
	 * Class constructor.
	 */
	public function __construct() {
		global $status, $page;
		parent::__construct(
			array(
				'singular' => 'vinkki',
				'plural'   => 'vinkit',
				'ajax'     => false,
			)
		);
	}

	/**
	 * Tulostaa kirjasarakkeen sisällön.
	 *
	 * @param WP_Post $item Tulostettava kirja.
	 */
	public function column_kirja( $item ) {
		$actions           = array(
			'edit' => '<a href="' . get_edit_post_link( $item->ID ) . '">Muokkaa</a>',
		);
		$kirjailijat       = mikko_get_authors( $item->ID );
		$alkukielinen_nimi = get_post_meta( $item->ID, 'alkuperäinen_nimi', true );
		return sprintf(
			'<strong><a href="%1$s">%2$s</a></strong> – <em>%3$s</em> %4$s',
			get_permalink( $item->ID ),
			$kirjailijat . $item->post_title,
			$alkukielinen_nimi,
			$this->row_actions( $actions ),
		);
	}

	/**
	 * Tulostaa checkbox-sarakkeen sisällön.
	 *
	 * @param WP_Post $item Rivin post object.
	 */
	public function column_cb( $item ) {
		return sprintf(
			'<input type="checkbox" name="%1$s[]" value="%2$s" />',
			$this->_args['singular'],
			$item->ID
		);
	}

	/**
	 * Palauttaa sarakkeet.
	 *
	 * @return array Sisäinen nimi => näkyvä nimi.
	 */
	public function get_columns() {
		$columns = array(
			'cb'    => '<input type="checkbox">',
			'kirja' => 'Kirja',
		);

		return $columns;
	}

	/**
	 * Palauttaa massatoiminnot.
	 *
	 * @return array Massatoiminnot.
	 */
	public function get_bulk_actions() {
		$actions = array(
			'edit' => 'Aseta kieli',
		);

		return $actions;
	}

	/**
	 * Tulostaa massatoiminnot.
	 *
	 * @param string $which 'top' tai 'bottom', riippuen siitä näytetäänkö ylä-
	 * vai alapuolen toiminnot.
	 */
	public function bulk_actions( $which = '' ) {
		if ( 'bottom' === $which ) {
			return;
		}
		?>
		<div class="alignleft actions bulkactions">
			<input type="hidden" id="action" name="action" value="edit" />
			Alkukieli: <input type="text" id="alkukieli" name="alkukieli" />
			<input type="submit" name="bulk_edit" id="bulk_edit" class="button button-primary" value="Päivitä" />
		</div>
		<?php
	}

	/**
	 * Prosessoi massatoiminnot.
	 *
	 * Lukee tiedot $_REQUEST-muuttujasta ja lisää kieliasetukset sen
	 * mukaisesti.
	 */
	public function process_bulk_action() {
		if ( isset( $_REQUEST['_wpnonce'] ) ) {
			wp_verify_nonce( $_REQUEST['_wpnonce'], 'bulk-' . $this->_args['plural'] );
		}

		if ( isset( $_REQUEST['action'] ) && 'edit' === $_REQUEST['action'] ) {
			$alkukieli = $_REQUEST['alkukieli'] ?? '';
			$vinkit    = $_REQUEST['vinkki'] ?? array();

			if ( empty( $alkukieli ) || empty( $vinkit ) ) {
				echo '<div class="notice notice-error">Mitään ei valittu.</div>';
				return;
			}

			$mitä_tehtiin = '';
			foreach ( $vinkit as $vinkki ) {
				$onnistui = wp_set_post_terms( $vinkki, $alkukieli, 'alkukieli', true );
				if ( $onnistui ) {
					$kirja         = get_the_title( $vinkki );
					$mitä_tehtiin .= '<div class="notice notice-success">Kirjalle <em>' . $kirja . '</em> lisättiin kieleksi "' . $alkukieli . '".</div>';
				} else {
					$kirja         = get_the_title( $vinkki );
					$mitä_tehtiin .= '<div class="notice notice-warning">Kirjan <em>' . $kirja . '</em> päivittäminen epäonnistui.</div>';
				}
			}
			echo $mitä_tehtiin; // phpcs:ignore
		}
	}

	/**
	 * Hakee artikkelit esitettäväksi listassa.
	 */
	public function prepare_items() {
		if ( isset( $_REQUEST['_wpnonce'] ) ) {
			wp_verify_nonce( $_REQUEST['_wpnonce'], 'bulk-' . $this->_args['plural'] );
		}

		$this->process_bulk_action();

		$tax_query = array(
			'relation' => 'AND',
			array(
				'taxonomy' => 'alkukieli',
				'operator' => 'NOT EXISTS',
			),
			array(
				'taxonomy' => 'luokka',
				'terms'    => 'käännös',
				'field'    => 'name',
				'operator' => 'IN',
			),
		);

		$kaikki = get_posts(
			array(
				'post_type'      => 'post',
				'post_status'    => 'publish',
				'tax_query'      => $tax_query,
				'posts_per_page' => -1,
				'fields'         => 'ids',
			),
		);

		$total_items = count( $kaikki );

		$paged    = isset( $_REQUEST['paged'] ) ? max( 0, intval( $_REQUEST['paged'] ) ) : 0;
		$per_page = 25;

		$this->items = get_posts(
			array(
				'post_type'      => 'post',
				'post_status'    => 'publish',
				'tax_query'      => $tax_query,
				'posts_per_page' => $per_page,
				'paged'          => $paged,
			)
		);

		$columns = $this->get_columns();
		$hidden  = array();

		$this->_column_headers = array( $columns, $hidden, array() );

		$this->set_pagination_args(
			array(
				'total_items' => $total_items,
				'per_page'    => $per_page,
				'total_pages' => ceil( $total_items / $per_page ),
			)
		);
	}
}

Tässä prepare_items()-funktion alussa tarkistetaan nonce ja sen jälkeen ajetaan process_bulk_action(), joka tarkistaa, onko bulkkitoimintoja tehty. Jos on, se tekee valittuihin artikkeleihin tehtävät muutokset. Lisäksi tarvitaan get_bulk_actions()-funktio, joka palauttaa listan mahdollisista toiminnoista ja bulk_actions(), joka tulostaa bulkkitoimintojen kontrollit.

Kun tehdään bulkkitoimintoja, taulussa on luonnollisesti oltava mukana checkbox-sarake, josta voi rastittaa ruutuja.

Vastaa

Sähköpostiosoitettasi ei julkaista. Pakolliset kentät on merkitty *

This site uses Akismet to reduce spam. Learn how your comment data is processed.